// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "net/url_request/sdch_dictionary_fetcher.h"

#include <queue>
#include <set>
#include <stdint.h>

#include "base/auto_reset.h"
#include "base/bind.h"
#include "base/compiler_specific.h"
#include "base/macros.h"
#include "base/threading/thread_task_runner_handle.h"
#include "net/base/io_buffer.h"
#include "net/base/load_flags.h"
#include "net/base/sdch_net_log_params.h"
#include "net/http/http_response_headers.h"
#include "net/log/net_log.h"
#include "net/url_request/redirect_info.h"
#include "net/url_request/url_request_context.h"
#include "net/url_request/url_request_status.h"
#include "net/url_request/url_request_throttler_manager.h"

namespace net {

namespace {

    const int kBufferSize = 4096;

    // Map the bytes_read result from a read attempt and a URLRequest's
    // status into a single net return value.
    int GetReadResult(int bytes_read, const URLRequest* request)
    {
        int rv = request->status().error();
        if (request->status().is_success() && bytes_read < 0) {
            rv = ERR_FAILED;
            request->net_log().AddEventWithNetErrorCode(
                NetLog::TYPE_SDCH_DICTIONARY_FETCH_IMPLIED_ERROR, rv);
        }

        if (rv == OK)
            rv = bytes_read;

        return rv;
    }

    struct FetchInfo {
        FetchInfo(const GURL& url,
            bool cache_only,
            const SdchDictionaryFetcher::OnDictionaryFetchedCallback& callback)
            : url(url)
            , cache_only(cache_only)
            , callback(callback)
        {
        }
        FetchInfo() { }

        GURL url;
        bool cache_only;
        SdchDictionaryFetcher::OnDictionaryFetchedCallback callback;
    };

} // namespace

// A UniqueFetchQueue is used to queue outgoing requests, which are either cache
// requests or network requests (which *may* still be served from cache).
// The UniqueFetchQueue enforces that a URL can only be queued for network fetch
// at most once. Calling Clear() resets UniqueFetchQueue's memory of which URLs
// have been queued.
class SdchDictionaryFetcher::UniqueFetchQueue {
public:
    UniqueFetchQueue();
    ~UniqueFetchQueue();

    bool Push(const FetchInfo& info);
    bool Pop(FetchInfo* info);
    bool IsEmpty() const;
    void Clear();

private:
    std::queue<FetchInfo> queue_;
    std::set<GURL> ever_network_queued_;

    DISALLOW_COPY_AND_ASSIGN(UniqueFetchQueue);
};

SdchDictionaryFetcher::UniqueFetchQueue::UniqueFetchQueue() { }
SdchDictionaryFetcher::UniqueFetchQueue::~UniqueFetchQueue() { }

bool SdchDictionaryFetcher::UniqueFetchQueue::Push(const FetchInfo& info)
{
    if (ever_network_queued_.count(info.url) != 0)
        return false;
    if (!info.cache_only)
        ever_network_queued_.insert(info.url);
    queue_.push(info);
    return true;
}

bool SdchDictionaryFetcher::UniqueFetchQueue::Pop(FetchInfo* info)
{
    if (IsEmpty())
        return false;
    *info = queue_.front();
    queue_.pop();
    return true;
}

bool SdchDictionaryFetcher::UniqueFetchQueue::IsEmpty() const
{
    return queue_.empty();
}

void SdchDictionaryFetcher::UniqueFetchQueue::Clear()
{
    ever_network_queued_.clear();
    while (!queue_.empty())
        queue_.pop();
}

SdchDictionaryFetcher::SdchDictionaryFetcher(URLRequestContext* context)
    : next_state_(STATE_NONE)
    , in_loop_(false)
    , fetch_queue_(new UniqueFetchQueue())
    , context_(context)
{
    DCHECK(CalledOnValidThread());
    DCHECK(context);
}

SdchDictionaryFetcher::~SdchDictionaryFetcher()
{
}

bool SdchDictionaryFetcher::Schedule(
    const GURL& dictionary_url,
    const OnDictionaryFetchedCallback& callback)
{
    return ScheduleInternal(dictionary_url, false, callback);
}

bool SdchDictionaryFetcher::ScheduleReload(
    const GURL& dictionary_url,
    const OnDictionaryFetchedCallback& callback)
{
    return ScheduleInternal(dictionary_url, true, callback);
}

void SdchDictionaryFetcher::Cancel()
{
    DCHECK(CalledOnValidThread());

    ResetRequest();
    next_state_ = STATE_NONE;

    fetch_queue_->Clear();
}

void SdchDictionaryFetcher::OnReceivedRedirect(
    URLRequest* request,
    const RedirectInfo& redirect_info,
    bool* defer_redirect)
{
    DCHECK_EQ(next_state_, STATE_SEND_REQUEST_PENDING);

    next_state_ = STATE_RECEIVED_REDIRECT;

    DoLoop(OK);
}

void SdchDictionaryFetcher::OnResponseStarted(URLRequest* request)
{
    DCHECK(CalledOnValidThread());
    DCHECK_EQ(request, current_request_.get());
    DCHECK_EQ(next_state_, STATE_SEND_REQUEST_PENDING);
    DCHECK(!in_loop_);

    // Confirm that the response isn't a stale read from the cache (as
    // may happen in the reload case).  If the response was not retrieved over
    // HTTP, it is presumed to be fresh.
    HttpResponseHeaders* response_headers = request->response_headers();
    int result = request->status().error();
    if (result == OK && response_headers) {
        ValidationType validation_type = response_headers->RequiresValidation(
            request->response_info().request_time,
            request->response_info().response_time, base::Time::Now());
        // TODO(rdsmith): Maybe handle VALIDATION_ASYNCHRONOUS by queueing
        // a non-reload request for the dictionary.
        if (validation_type != VALIDATION_NONE)
            result = ERR_FAILED;
    }

    DoLoop(result);
}

void SdchDictionaryFetcher::OnReadCompleted(URLRequest* request,
    int bytes_read)
{
    DCHECK(CalledOnValidThread());
    DCHECK_EQ(request, current_request_.get());
    DCHECK_EQ(next_state_, STATE_READ_BODY_COMPLETE);
    DCHECK(!in_loop_);

    DoLoop(GetReadResult(bytes_read, current_request_.get()));
}

bool SdchDictionaryFetcher::ScheduleInternal(
    const GURL& dictionary_url,
    bool reload,
    const OnDictionaryFetchedCallback& callback)
{
    DCHECK(CalledOnValidThread());

    // If Push() fails, |dictionary_url| has already been fetched or scheduled to
    // be fetched.
    if (!fetch_queue_->Push(FetchInfo(dictionary_url, reload, callback))) {
        // TODO(rdsmith): Log this error to the net log.  In the case of a
        // normal fetch, this can be through the URLRequest
        // initiating this fetch (once the URLRequest is passed to the fetcher);
        // in the case of a reload, it's more complicated.
        SdchManager::SdchErrorRecovery(
            SDCH_DICTIONARY_PREVIOUSLY_SCHEDULED_TO_DOWNLOAD);
        return false;
    }

    // If the loop is already processing, it'll pick up the above in the
    // normal course of events.
    if (next_state_ != STATE_NONE)
        return true;

    next_state_ = STATE_SEND_REQUEST;

    // There are no callbacks to user code from the dictionary fetcher,
    // and Schedule() is only called from user code, so this call to DoLoop()
    // does not require an |if (in_loop_) return;| guard.
    DoLoop(OK);
    return true;
}

void SdchDictionaryFetcher::ResetRequest()
{
    current_request_.reset();
    buffer_ = nullptr;
    current_callback_.Reset();
    dictionary_.reset();
    return;
}

int SdchDictionaryFetcher::DoLoop(int rv)
{
    DCHECK(!in_loop_);
    base::AutoReset<bool> auto_reset_in_loop(&in_loop_, true);

    do {
        State state = next_state_;
        next_state_ = STATE_NONE;
        switch (state) {
        case STATE_SEND_REQUEST:
            rv = DoSendRequest(rv);
            break;
        case STATE_RECEIVED_REDIRECT:
            rv = DoReceivedRedirect(rv);
            break;
        case STATE_SEND_REQUEST_PENDING:
            rv = DoSendRequestPending(rv);
            break;
        case STATE_READ_BODY:
            rv = DoReadBody(rv);
            break;
        case STATE_READ_BODY_COMPLETE:
            rv = DoReadBodyComplete(rv);
            break;
        case STATE_REQUEST_COMPLETE:
            rv = DoCompleteRequest(rv);
            break;
        case STATE_NONE:
            NOTREACHED();
        }
    } while (rv != ERR_IO_PENDING && next_state_ != STATE_NONE);

    return rv;
}

int SdchDictionaryFetcher::DoSendRequest(int rv)
{
    DCHECK(CalledOnValidThread());

    // |rv| is ignored, as the result from the previous request doesn't
    // affect the next request.

    if (fetch_queue_->IsEmpty() || current_request_.get()) {
        next_state_ = STATE_NONE;
        return OK;
    }

    next_state_ = STATE_SEND_REQUEST_PENDING;

    FetchInfo info;
    bool success = fetch_queue_->Pop(&info);
    DCHECK(success);
    current_request_ = context_->CreateRequest(info.url, IDLE, this);
    int load_flags = LOAD_DO_NOT_SEND_COOKIES | LOAD_DO_NOT_SAVE_COOKIES;
    if (info.cache_only)
        load_flags |= LOAD_ONLY_FROM_CACHE;
    current_request_->SetLoadFlags(load_flags);

    buffer_ = new IOBuffer(kBufferSize);
    dictionary_.reset(new std::string());
    current_callback_ = info.callback;

    current_request_->Start();
    current_request_->net_log().AddEvent(NetLog::TYPE_SDCH_DICTIONARY_FETCH);

    return ERR_IO_PENDING;
}

int SdchDictionaryFetcher::DoReceivedRedirect(int rv)
{
    // Fetching SDCH through a redirect is forbidden; it raises possible
    // security issues cross-origin, and isn't obviously useful within
    // an origin.
    ResetRequest();
    next_state_ = STATE_SEND_REQUEST;
    return ERR_UNSAFE_REDIRECT;
}

int SdchDictionaryFetcher::DoSendRequestPending(int rv)
{
    DCHECK(CalledOnValidThread());

    // If there's been an error, abort the current request.
    if (rv != OK) {
        ResetRequest();
        next_state_ = STATE_SEND_REQUEST;
        return OK;
    }

    next_state_ = STATE_READ_BODY;
    return OK;
}

int SdchDictionaryFetcher::DoReadBody(int rv)
{
    DCHECK(CalledOnValidThread());

    // If there's been an error, abort the current request.
    if (rv != OK) {
        ResetRequest();
        next_state_ = STATE_SEND_REQUEST;
        return OK;
    }

    next_state_ = STATE_READ_BODY_COMPLETE;
    int bytes_read = 0;
    current_request_->Read(buffer_.get(), kBufferSize, &bytes_read);
    if (current_request_->status().is_io_pending())
        return ERR_IO_PENDING;

    return GetReadResult(bytes_read, current_request_.get());
}

int SdchDictionaryFetcher::DoReadBodyComplete(int rv)
{
    DCHECK(CalledOnValidThread());

    // An error; abort the current request.
    if (rv < 0) {
        ResetRequest();
        next_state_ = STATE_SEND_REQUEST;
        return OK;
    }

    DCHECK(current_request_->status().is_success());

    // Data; append to the dictionary and look for more data.
    if (rv > 0) {
        dictionary_->append(buffer_->data(), rv);
        next_state_ = STATE_READ_BODY;
        return OK;
    }

    // End of file; complete the request.
    next_state_ = STATE_REQUEST_COMPLETE;
    return OK;
}

int SdchDictionaryFetcher::DoCompleteRequest(int rv)
{
    DCHECK(CalledOnValidThread());

    // If the dictionary was successfully fetched, add it to the manager.
    if (rv == OK) {
        current_callback_.Run(*dictionary_, current_request_->url(),
            current_request_->net_log(),
            current_request_->was_cached());
    }

    ResetRequest();
    next_state_ = STATE_SEND_REQUEST;
    return OK;
}

} // namespace net
